package com.erikpasternak.vader;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.widget.Toast;

public class AdkHelper {
    private static final String TAG = "AdkHelper";
    private static final boolean DEBUG = false;

    private static final String ACTION_USB_PERMISSION = "com.google.android.Bender.action.USB_PERMISSION";

    public static final byte ADK_COMMAND_BUTTON = 1;
    public static final byte ADK_COMMAND_INGREDIENT_DONE = 2;

    public static final byte COMMAND_MIX = 1;

    private static final int MSG_START_LISTENING = 0;
    private static final int MSG_STOP_LISTENING = 1;
    private static final int MSG_SEND_COMMAND = 2;
    private static final int MSG_RECEIVED_COMMAND = 3;

    public static final byte[] RETURN_MSG_VALUES = new byte[] {
            (byte) 0, (byte) 0
    };

    public static final byte[] CLEAN_MSG_VALUES = new byte[] {
            (byte) 4, (byte) 0
    };

    private PendingIntent mPermissionIntent;
    private boolean mPermissionRequestPending;

    private UsbManager mUsbManager;
    private FileInputStream mInputStream;
    private FileOutputStream mOutputStream;
    private UsbAccessory mAccessory;
    private ParcelFileDescriptor mFileDescriptor;
    private final Context mContext;
    private final Thread mThread;
    private DataReader mDataReader;
    private Handler mHandler;
    private final Handler mListenerHandler;
    private final ArrayList<Listener> mListeners;

    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Toast.makeText(mContext, "received " + action, Toast.LENGTH_SHORT).show();
            Log.d(TAG, "Received action " + action);
            if (ACTION_USB_PERMISSION.equals(action)) {
                setupAccessory(intent);
            } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
                UsbAccessory accessory = (UsbAccessory) intent
                        .getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
                synchronized (this) {
                    if (accessory != null && accessory.equals(mAccessory)) {
                        closeAccessory();
                    }
                    if (DEBUG) {
                        Toast.makeText(mContext, "Disconnected Adk", Toast.LENGTH_SHORT).show();
                    }
                }
                mHandler.sendEmptyMessage(MSG_STOP_LISTENING);
            } else if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED.equals(action)) {
                setupAccessory(intent);
            }
        }

        public void setupAccessory(Intent intent) {
            synchronized (this) {
                UsbAccessory accessory = (UsbAccessory) intent
                        .getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
                if (intent.getBooleanExtra(
                        UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    openAccessory(accessory);

                    if (DEBUG) {
                        Toast.makeText(mContext, "Connected Adk", Toast.LENGTH_SHORT).show();
                    }
                } else {
                    if (DEBUG) {
                        Toast.makeText(mContext, "permission denied", Toast.LENGTH_SHORT).show();
                    }
                    Log.d(TAG, "permission denied for accessory "
                            + accessory);
                }
                mPermissionRequestPending = false;
            }
        }
    };

    public interface Listener {
        public void onMessageReceived(int command, int length, byte[] data);

        public void onSendFailed(int command);
    }

    public class DataReader extends Thread {
        private FileInputStream input;

        public DataReader(FileInputStream in) {
            super();
            this.input = in;
        }

        @Override
        public void run() {
            int numRead = 0;
            byte[] buffer = new byte[1024];
            try {
                while ((numRead = input.read(buffer)) != -1) {
                    if (numRead > 0) {
                        Message msg = new Message();
                        msg.what = MSG_RECEIVED_COMMAND;
                        msg.arg1 = buffer[0];
                        msg.arg2 = buffer[1];
                        mHandler.sendMessage(msg);
                    }
                }
            } catch (Exception e) {
                Log.d(TAG, "Oh noes, reading failed");
            }
        }
    }

    public AdkHelper(Context context) {
        mContext = context;
        if (mUsbManager == null) {
            mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);

            mPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(
                    ACTION_USB_PERMISSION), 0);
        }
        mListeners = new ArrayList<Listener>();
        mListenerHandler = new Handler();

        mThread = new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                synchronized (mThread) {
                    mHandler = new Handler() {
                        private final boolean mListening = false;

                        @Override
                        public void handleMessage(Message msg) {
                            if (DEBUG) {
                                Toast.makeText(mContext, "handle message " + msg.what,
                                        Toast.LENGTH_SHORT).show();
                            }
                            switch (msg.what) {
                                case MSG_START_LISTENING:
                                    if (mInputStream != null) {
                                        if (mDataReader != null) {
                                            try {
                                                mDataReader.stop();
                                            } catch (Exception e) {
                                                Log.d(TAG, "Oh noes, can't stop the thread.");
                                            }
                                        }
                                        mDataReader = new DataReader(mInputStream);
                                        mDataReader.start();
                                    }
                                    break;
                                case MSG_RECEIVED_COMMAND:
                                    processCommand(msg.arg1, msg.arg2);
                                    break;
                                case MSG_STOP_LISTENING:
                                    removeMessages(MSG_START_LISTENING);
                                    break;
                                case MSG_SEND_COMMAND:
                                    synchronized (mUsbReceiver) {
                                        sendCommandInternal((byte) msg.arg1, (byte) msg.arg2,
                                                (byte[]) msg.obj);
                                    }
                                    break;
                            }

                        }
                    };
                    mThread.notify();
                }
                Looper.loop();
            }
        };
        mThread.start();
    }

    public void onNewIntent(Intent intent) {
        mUsbReceiver.onReceive(mContext, intent);
    }

    public void addListener(Listener listener) {
        mListeners.add(listener);
    }

    public void removeListener(Listener listener) {
        mListeners.remove(listener);
    }

    public boolean onResume() {

        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED);
        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
        mContext.registerReceiver(mUsbReceiver, filter);

        synchronized (mThread) {
            try {
                if (mHandler == null) {
                    mThread.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (mInputStream != null) {
            mHandler.sendEmptyMessage(MSG_START_LISTENING);
        }

        UsbAccessory[] accessories = mUsbManager.getAccessoryList();
        Log.d(TAG, "accessories list is " + accessories);
        Toast.makeText(mContext, "Resuming, accessories are " + accessories, Toast.LENGTH_SHORT);
        UsbAccessory accessory = (accessories == null ? null : accessories[0]);

        if (accessory != null) {
            if (mUsbManager.hasPermission(accessory)) {
                openAccessory(accessory);
                Toast.makeText(mContext, "Ready to go", Toast.LENGTH_SHORT).show();
            } else {
                synchronized (mUsbReceiver) {
                    if (!mPermissionRequestPending) {
                        mUsbManager.requestPermission(accessory,
                                mPermissionIntent);
                        mPermissionRequestPending = true;
                    }
                }
            }
        } else {
            Log.d(TAG, "mAccessory is null");
            return false;
        }
        return true;
    }

    public void onPause() {
        mContext.unregisterReceiver(mUsbReceiver);
        closeAccessory();
    }

    private void openAccessory(UsbAccessory accessory) {
        if (mFileDescriptor != null) {
            if (DEBUG) {
                Toast.makeText(mContext, "Already open", Toast.LENGTH_SHORT).show();
            }
            return;
        }
        if (DEBUG) {
            Toast.makeText(mContext, "opening accessory", Toast.LENGTH_SHORT).show();
        }
        mFileDescriptor = mUsbManager.openAccessory(accessory);
        if (DEBUG) {
            Toast.makeText(mContext, "initial open done fd " + mFileDescriptor, Toast.LENGTH_SHORT)
                    .show();
        }

        if (mFileDescriptor != null) {
            mAccessory = accessory;
            FileDescriptor fd = mFileDescriptor.getFileDescriptor();
            mInputStream = new FileInputStream(fd);
            mOutputStream = new FileOutputStream(fd);
            mHandler.sendEmptyMessage(MSG_START_LISTENING);
            if (DEBUG) {
                Toast.makeText(mContext, "streams set up", Toast.LENGTH_SHORT).show();
            }
            sendCommand(COMMAND_MIX, (byte) RETURN_MSG_VALUES.length, RETURN_MSG_VALUES);
            Log.d(TAG, "accessory opened");
        } else {
            Log.d(TAG, "accessory open fail");
        }
    }

    private void closeAccessory() {

        mHandler.sendEmptyMessage(MSG_STOP_LISTENING);
        try {
            if (mInputStream != null) {
                mInputStream.close();
            }
        } catch (IOException e) {

        } finally {
            mInputStream = null;
        }

        try {
            if (mOutputStream != null) {
                mOutputStream.close();
            }
        } catch (IOException e) {

        } finally {
            mOutputStream = null;
        }

        try {
            if (mFileDescriptor != null) {
                mFileDescriptor.close();
            }
        } catch (IOException e) {
        } finally {
            mFileDescriptor = null;
        }
    }

    public boolean isReady() {
        return mInputStream != null && mOutputStream != null;
    }

    public void sendTestCommand() {
        Message testMsg = new Message();
        testMsg.what = MSG_SEND_COMMAND;
        testMsg.arg1 = 1;
        testMsg.arg2 = 4;
        testMsg.obj = new byte[] {
                1, 5, 4, 2
        };
        mHandler.sendMessage(testMsg);
    }

    public void sendCommand(byte command, byte length, byte[] values) {
        if (DEBUG) {
            Toast.makeText(mContext, "send " + command + " l " + length, Toast.LENGTH_SHORT).show();
        }
        Message msg = new Message();
        msg.what = MSG_SEND_COMMAND;
        msg.arg1 = command;
        msg.arg2 = length;
        msg.obj = values;
        mHandler.sendMessage(msg);
    }

    private void sendCommandInternal(final byte command, byte length, byte[] values) {
        byte[] buffer = new byte[length + 2];

        buffer[0] = command;
        buffer[1] = length;
        for (int i = 0; i < values.length; i++) {
            buffer[i + 2] = values[i];
        }
        if (DEBUG) {
            Toast.makeText(mContext, "sending message " + Arrays.toString(buffer) + " OS is "
                    + mOutputStream, Toast.LENGTH_SHORT).show();
        }
        if (mOutputStream != null && buffer[1] != -1) {
            try {
                mOutputStream.write(buffer);
            } catch (IOException e) {
                postCommandFailed(command);
                Log.e(TAG, "write failed", e);
            }
        } else {
            postCommandFailed(command);
            Log.d(TAG,
                    "No device connected, failed to send " + command + " of length " + length
                            + " with vals " + Arrays.toString(values));
        }
    }

    public void postCommandFailed(final byte command) {
        mListenerHandler.post(new Runnable() {
            public void run() {
                for (Listener listener : mListeners) {
                    listener.onSendFailed(command);
                }
            }
        });
    }

    public void processCommand(int command, int length) {
        postMessageReceived(command, length, null);
    }

    public void postMessageReceived(final int command, final int length, final byte[] data) {
        mListenerHandler.post(new Runnable() {
            public void run() {
                for (Listener listener : mListeners) {
                    listener.onMessageReceived(command, length, data);
                }
            }
        });
    }

}
